package cn.trigram.math;

import static ch.obermuhlner.math.big.DefaultBigDecimalMath.currentMathContext;

import java.math.BigDecimal;
import java.util.function.Function;

/**
 * 牛顿迭代法
 */
public class BigDecimalNewtonItrMethod implements NewtonItrMethod<BigDecimal> {

  private static final BigDecimal two = new BigDecimal("2", currentMathContext());

  @Override
  public BigDecimal func(BigDecimal x) {

    throw new UnsupportedOperationException();
  }

  @Override
  public BigDecimal deriveFunc(BigDecimal x) {
    // 如果导函数存在为0的情况，默认用差商代替
    return differenceQuotient(x, x.divide(two, currentMathContext()));
  }

  @Override
  public BigDecimal differenceQuotient(BigDecimal x0, BigDecimal x1) {

    BigDecimal numerator   = func(x0).subtract(func(x1), currentMathContext());
    BigDecimal denominator = x0.subtract(x1, currentMathContext());
    return numerator.divide(denominator, currentMathContext());
  }

  @Override
  public BigDecimal fakePoint(BigDecimal x0, BigDecimal x1) {
    // 公式实际：x0 - f(x0)/f`(x0)，但用差商代替导数
    BigDecimal numerator   = func(x1).multiply((x0.subtract(x1, currentMathContext())), currentMathContext());
    BigDecimal denominator = func(x0).subtract(func(x1), currentMathContext());
    BigDecimal diff        = numerator.divide(denominator, currentMathContext());
    return x1.subtract(diff, currentMathContext());
  }

  @Override
  public BigDecimal approximate(BigDecimal x0, BigDecimal x2, BigDecimal X) {
    // 跟伪点公式一样，只能用的点变了
    BigDecimal numerator   = func(x2).multiply(x0.subtract(X, currentMathContext()), currentMathContext());
    BigDecimal denominator = func(x0).subtract(func(X), currentMathContext());
    BigDecimal diff        = numerator.divide(denominator, currentMathContext());
    return x2.subtract(diff, currentMathContext());
  }

  @Override
  public BigDecimal calc(BigDecimal x0, BigDecimal error) {
    // 原牛顿迭代公式求第一个点
    BigDecimal x1 = x0.subtract(func(x0).divide(deriveFunc(x0), currentMathContext()), currentMathContext());
    // 求伪点
    BigDecimal X = fakePoint(x0, x1);
    // 求第三个点
    BigDecimal x2 = (x1.add(X, currentMathContext())).divide(two, currentMathContext());

    BigDecimal x3 = approximate(x0, x2, X);
//    System.out.println("第1次");
    int n = 2;
    Function<BigDecimal, BigDecimal> abs = x -> x.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO.subtract(
        x, currentMathContext()) : x;
    BigDecimal lastX3 = x3;
    // 代入原函数小于误差值就退出
    while (!(abs.apply(func(x3)).compareTo(error) <= 0)) {
//      System.out.printf("第%d次%n", n++);
      x0 = x1;
      x1 = x2;
      X  = fakePoint(x0, x1);
      x2 = x3;
      x3 = approximate(x0, x2, X);
      if (x3.compareTo(lastX3) == 0) {
        // 防止相等但小数位又太小时继续运算
        break;
      }
      lastX3 = x3;
    }
    return x3;
  }

}
